﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NLog;
using NGinnBPM.Lib.Interfaces;
using NGinnBPM.Lib;
using NGinnBPM.MessageBus.Impl.HttpService;
using NGinnBPM.MessageBus;
using System.IO;
using NGinnBPM.Lib.Schema;
using NGinnBPM.Lib.Data;
using System.Xml;
using System.Xml.Xsl;

namespace NGinnBPM.Services.Website
{
    /// <summary>
    /// Dynamically generated web services for controlling 
    /// processes. Each process gets its own web service with
    /// message structure reflecting the internal process data structures.
    /// </summary>
    [UrlPattern(@"^/procws/(?<definition>[\w\.]+)?(/wsdl)?")]
    public class ProcessControlWSServlet : ServletBase
    {
        public INGEnvironment Environment { get; set; }
        public IProcessPackageRepository PackageRepository { get; set; }
        public string WSDLXslStylesheet { get; set; }
        public string SOAPResponseStylesheet { get; set; }

        public override void HandleRequest(IRequestContext ctx)
        {
            
            string dn = ctx.UrlVariables["definition"];
            if (string.IsNullOrEmpty(dn))
            {
                if (ctx.HttpMethod.Equals("GET", StringComparison.InvariantCultureIgnoreCase))
                {
                    ListAvailableWebServices(ctx.Input, ctx.Output, ctx);
                }
                else
                {
                    throw new Exception("Invalid http method");
                }
            }
            else
            {
                if (ctx.RawUrl.EndsWith("/?wsdl", StringComparison.InvariantCultureIgnoreCase))
                {
                    if (ctx.HttpMethod == "GET")
                    {
                        OutputServiceWSDL(ctx.Input, ctx.Output, ctx);
                    }
                    else
                        throw new Exception("Invalid http method");
                }
                else
                {
                    if (ctx.HttpMethod == "GET")
                    {
                        OutputServiceInformation(ctx.Input, ctx.Output, ctx);
                    }
                    else if (ctx.HttpMethod == "POST")
                    {
                        HandleWebServiceInvocation(ctx.Input, ctx.Output, ctx);
                    }
                    else
                        throw new Exception("http method not supported");
                }
            }

        }

        private void ListAvailableWebServices(TextReader input, TextWriter output, IRequestContext ctx)
        {
            output.WriteLine("available services");
            foreach (string pkg in PackageRepository.PackageNames)
            {
                PackageDefinition pd = PackageRepository.GetPackage(pkg);
                foreach (ProcessDefinition pdef in pd.ProcessDefinitions)
                {
                    output.WriteLine(pdef.DefinitionId);
                }
            }
        }

        private static void WriteXSElementDeclaration(string name, string type, string minOccurs, string maxOccurs, XmlWriter xw)
        {
            xw.WriteStartElement("element", XmlSchemaUtil.SCHEMA_NS);
            xw.WriteAttributeString("name", name);
            xw.WriteAttributeString("type", type);
            xw.WriteAttributeString("minOccurs", minOccurs);
            xw.WriteAttributeString("maxOccurs", maxOccurs);
            xw.WriteEndElement();
        }

        private void WriteWsdlMessagesForTaskOperation(string opname, XmlWriter xw)
        {
            xw.WriteStartElement("message", XmlSchemaUtil.WSDL_NS);
            xw.WriteAttributeString("name", string.Format("{0}_SoapIn", opname));
            xw.WriteStartElement("part", XmlSchemaUtil.WSDL_NS);
            xw.WriteAttributeString("name", "parameters");
            xw.WriteAttributeString("element", string.Format("tns:{0}", opname));
            xw.WriteEndElement();
            xw.WriteEndElement();

            xw.WriteStartElement("message", XmlSchemaUtil.WSDL_NS);
            xw.WriteAttributeString("name", string.Format("{0}_SoapOut", opname));
            xw.WriteStartElement("part", XmlSchemaUtil.WSDL_NS);
            xw.WriteAttributeString("name", "parameters");
            xw.WriteAttributeString("element", string.Format("tns:{0}_Response", opname));
            xw.WriteEndElement();
            xw.WriteEndElement();
        }

        private void WriteWsdlOperation(string opname, XmlWriter xw)
        {
            xw.WriteStartElement("operation", XmlSchemaUtil.WSDL_NS);
            xw.WriteAttributeString("name", opname);
            xw.WriteStartElement("input", XmlSchemaUtil.WSDL_NS);
            xw.WriteAttributeString("message", string.Format("tns:{0}_SoapIn", opname));
            xw.WriteEndElement();
            xw.WriteStartElement("output", XmlSchemaUtil.WSDL_NS);
            xw.WriteAttributeString("message", string.Format("tns:{0}_SoapOut", opname));
            xw.WriteEndElement();
            xw.WriteEndElement();
        }

        private void WriteWsdlSoapOperationBinding(string opname, XmlWriter xw, IRequestContext ctx)
        {
            xw.WriteStartElement("operation", XmlSchemaUtil.WSDL_NS);
            xw.WriteAttributeString("name", opname);
            xw.WriteStartElement("operation", XmlSchemaUtil.SOAP_NS);
            xw.WriteAttributeString("soapAction", ctx.Url.ToString().Replace(ctx.Url.Query, "") + opname);
            xw.WriteAttributeString("style", "document");
            xw.WriteEndElement();
            xw.WriteStartElement("input", XmlSchemaUtil.WSDL_NS);
            xw.WriteStartElement("body", XmlSchemaUtil.SOAP_NS);
            xw.WriteAttributeString("use", "literal");
            xw.WriteEndElement();
            xw.WriteEndElement();
            xw.WriteStartElement("output", XmlSchemaUtil.WSDL_NS);
            xw.WriteStartElement("body", XmlSchemaUtil.SOAP_NS);
            xw.WriteAttributeString("use", "literal");
            xw.WriteEndElement();
            xw.WriteEndElement();
            xw.WriteEndElement();
        }

        private void WriteWsdlSoap12OperationBinding(string opname, XmlWriter xw, IRequestContext ctx)
        {
            xw.WriteStartElement("operation", XmlSchemaUtil.WSDL_NS);
            xw.WriteAttributeString("name", opname);
            xw.WriteStartElement("operation", XmlSchemaUtil.SOAP12_NS);
            xw.WriteAttributeString("soapAction", ctx.Url.ToString().Replace(ctx.Url.Query, "") + opname);
            xw.WriteAttributeString("style", "document");
            xw.WriteEndElement();
            xw.WriteStartElement("input", XmlSchemaUtil.WSDL_NS);
            xw.WriteStartElement("body", XmlSchemaUtil.SOAP12_NS);
            xw.WriteAttributeString("use", "literal");
            xw.WriteEndElement();
            xw.WriteEndElement();
            xw.WriteStartElement("output", XmlSchemaUtil.WSDL_NS);
            xw.WriteStartElement("body", XmlSchemaUtil.SOAP12_NS);
            xw.WriteAttributeString("use", "literal");
            xw.WriteEndElement();
            xw.WriteEndElement();
            xw.WriteEndElement();
        }

        /// <summary>
        /// Web service
        /// - StartProcess
        /// - GetProcessData
        /// - UpdateProcessData
        /// - CompleteProcess
        /// - CancelProcess
        /// - FailProcess
        /// - CancelTask
        /// - FailTask
        /// - GetProcessStatusInformation
        /// task(for each)
        /// - GetTaskData
        /// - UpdateTaskData
        /// - CompleteTask
        /// 
        
        /// </summary>
        /// <param name="pd"></param>
        /// <param name="output"></param>
        /// <param name="ctx"></param>
        private void GenerateWsdl(ProcessDefinition pd, TextWriter output, IRequestContext ctx)
        {
            var tns = string.Format("http://nginn.org/ProcessWS_1_0/{0}.{1}", pd.PackageName, pd.Name);
            StringBuilder sb = new StringBuilder();
            XmlWriterSettings ws = new XmlWriterSettings();
            ws.OmitXmlDeclaration = true; ws.Indent = true;
            XmlWriter xw = XmlWriter.Create(output, ws);
            xw.WriteStartDocument();
            xw.WriteStartElement("wsdl", "definitions", XmlSchemaUtil.WSDL_NS);
            xw.WriteAttributeString("targetNamespace", tns);
            xw.WriteAttributeString("xmlns", "tns", null, tns);
            xw.WriteAttributeString("xmlns", "soap", null, XmlSchemaUtil.SOAP_NS);
            xw.WriteAttributeString("xmlns", "soap12", null, XmlSchemaUtil.SOAP12_NS);
            xw.WriteAttributeString("xmlns", "xs", null, XmlSchemaUtil.SCHEMA_NS);
            {//data types
                xw.WriteStartElement("types", XmlSchemaUtil.WSDL_NS);
                xw.WriteStartElement("xs", "schema", XmlSchemaUtil.SCHEMA_NS);
                xw.WriteAttributeString("targetNamespace", tns);

                TypeSet allTypes = new TypeSet();
                allTypes.ImportTypes(pd.DataTypes);
                pd.Body.GetAllTaskDataSchemas(allTypes);
                allTypes.WriteXmlSchema(xw, tns);
                {
                    xw.WriteStartElement("element", XmlSchemaUtil.SCHEMA_NS);
                    xw.WriteAttributeString("name", "StartProcess");
                    xw.WriteStartElement("complexType", XmlSchemaUtil.SCHEMA_NS);
                    xw.WriteStartElement("sequence", XmlSchemaUtil.SCHEMA_NS);
                    WriteXSElementDeclaration("input", string.Format("tns:Task_{0}_{1}_InputData", pd.Name, pd.Version), "1", "1", xw);
                    WriteXSElementDeclaration("createdBy", "xs:string", "0", "1", xw);
                    WriteXSElementDeclaration("correlationId", "xs:string", "0", "1", xw);
                    WriteXSElementDeclaration("parentTaskId", "xs:string", "0", "1", xw);
                    xw.WriteEndElement();
                    xw.WriteEndElement();
                    xw.WriteEndElement();

                    xw.WriteStartElement("element", XmlSchemaUtil.SCHEMA_NS);
                    xw.WriteAttributeString("name", "StartProcess_Response");
                    xw.WriteStartElement("complexType", XmlSchemaUtil.SCHEMA_NS);
                    xw.WriteStartElement("sequence", XmlSchemaUtil.SCHEMA_NS);
                    WriteXSElementDeclaration("StartProcessResult", "xs:string", "0", "1", xw);
                    xw.WriteEndElement();
                    xw.WriteEndElement();
                    xw.WriteEndElement();
                }
                foreach (Task t in pd.Body.GetTasksRecursive())
                {
                    string tid = t.Id.Replace('.', '_');
                    xw.WriteStartElement("element", XmlSchemaUtil.SCHEMA_NS);
                    xw.WriteAttributeString("name", "UpdateTaskData_" + tid);
                    xw.WriteStartElement("complexType", XmlSchemaUtil.SCHEMA_NS);
                    xw.WriteStartElement("sequence", XmlSchemaUtil.SCHEMA_NS);
                    WriteXSElementDeclaration("taskInstanceId", "xs:string", "1", "1", xw);
                    WriteXSElementDeclaration("taskData", string.Format("tns:Task_{0}_InternalData", tid), "1", "1", xw);
                    xw.WriteEndElement();
                    xw.WriteEndElement();
                    xw.WriteEndElement();

                    xw.WriteStartElement("element", XmlSchemaUtil.SCHEMA_NS);
                    xw.WriteAttributeString("name", string.Format("UpdateTaskData_{0}_Response", tid));
                    xw.WriteStartElement("complexType", XmlSchemaUtil.SCHEMA_NS);
                    xw.WriteEndElement();
                    xw.WriteEndElement();

                    xw.WriteStartElement("element", XmlSchemaUtil.SCHEMA_NS);
                    xw.WriteAttributeString("name", "CompleteTask_" + tid);
                    xw.WriteStartElement("complexType", XmlSchemaUtil.SCHEMA_NS);
                    xw.WriteStartElement("sequence", XmlSchemaUtil.SCHEMA_NS);
                    WriteXSElementDeclaration("taskInstanceId", "xs:string", "1", "1", xw);
                    WriteXSElementDeclaration("taskData", string.Format("tns:Task_{0}_InternalData", tid), "1", "1", xw);
                    xw.WriteEndElement();
                    xw.WriteEndElement();
                    xw.WriteEndElement();

                    xw.WriteStartElement("element", XmlSchemaUtil.SCHEMA_NS);
                    xw.WriteAttributeString("name", string.Format("CompleteTask_{0}_Response", tid));
                    xw.WriteStartElement("complexType", XmlSchemaUtil.SCHEMA_NS);
                    xw.WriteEndElement();
                    xw.WriteEndElement();

                    xw.WriteStartElement("element", XmlSchemaUtil.SCHEMA_NS);
                    xw.WriteAttributeString("name", "GetTaskData_" + tid);
                    xw.WriteStartElement("complexType", XmlSchemaUtil.SCHEMA_NS);
                    xw.WriteStartElement("sequence", XmlSchemaUtil.SCHEMA_NS);
                    WriteXSElementDeclaration("taskInstanceId", "xs:string", "1", "1", xw);
                    xw.WriteEndElement();
                    xw.WriteEndElement();
                    xw.WriteEndElement();

                    xw.WriteStartElement("element", XmlSchemaUtil.SCHEMA_NS);
                    xw.WriteAttributeString("name", string.Format("GetTaskData_{0}_Response", tid));
                    xw.WriteStartElement("complexType", XmlSchemaUtil.SCHEMA_NS);
                    xw.WriteStartElement("sequence", XmlSchemaUtil.SCHEMA_NS);
                    WriteXSElementDeclaration(string.Format("GetTaskData_{0}_Result", tid), string.Format("tns:Task_{0}_InternalData", tid), "0", "1", xw);
                    xw.WriteEndElement();
                    xw.WriteEndElement();
                    xw.WriteEndElement();
                }
                xw.WriteEndElement();
                xw.WriteEndElement();
            }//end of data types
            {//messages

                WriteWsdlMessagesForTaskOperation("StartProcess", xw);

                foreach (Task t in pd.Body.GetTasksRecursive())
                {
                    string tid = t.Id.Replace('.', '_');
                    WriteWsdlMessagesForTaskOperation(string.Format("UpdateTaskData_{0}", tid), xw);
                    WriteWsdlMessagesForTaskOperation(string.Format("CompleteTask_{0}", tid), xw);
                    WriteWsdlMessagesForTaskOperation(string.Format("GetTaskData_{0}", tid), xw);
                }
                
            }//end of messages
            {//port type
                xw.WriteStartElement("portType", XmlSchemaUtil.WSDL_NS);
                xw.WriteAttributeString("name", "Proc2Soap");

                WriteWsdlOperation("StartProcess", xw);

                
                foreach (Task t in pd.Body.GetTasksRecursive())
                {
                    string tid = t.Id.Replace('.', '_');
                    WriteWsdlOperation(string.Format("UpdateTaskData_{0}", tid), xw);
                    WriteWsdlOperation(string.Format("CompleteTask_{0}", tid), xw);
                    WriteWsdlOperation(string.Format("GetTaskData_{0}", tid), xw);
                }

                xw.WriteEndElement(); //portType
            }//end of port type
            {//soap binding
                xw.WriteStartElement("binding", XmlSchemaUtil.WSDL_NS);
                xw.WriteAttributeString("name", "Proc2Soap");
                xw.WriteAttributeString("type", "tns:Proc2Soap");

                xw.WriteStartElement("binding", XmlSchemaUtil.SOAP_NS);
                xw.WriteAttributeString("transport", "http://schemas.xmlsoap.org/soap/http");
                xw.WriteEndElement();

                WriteWsdlSoapOperationBinding("StartProcess", xw, ctx);
                foreach (Task t in pd.Body.GetTasksRecursive())
                {
                    string tid = t.Id.Replace('.', '_');
                    WriteWsdlSoapOperationBinding(string.Format("UpdateTaskData_{0}", tid), xw, ctx);
                    WriteWsdlSoapOperationBinding(string.Format("CompleteTask_{0}", tid), xw, ctx);
                    WriteWsdlSoapOperationBinding(string.Format("GetTaskData_{0}", tid), xw, ctx);
                }

                xw.WriteEndElement();
            }
            {//soap12 binding
                xw.WriteStartElement("binding", XmlSchemaUtil.WSDL_NS);
                xw.WriteAttributeString("name", "Proc2Soap12");
                xw.WriteAttributeString("type", "tns:Proc2Soap");

                xw.WriteStartElement("binding", XmlSchemaUtil.SOAP12_NS);
                xw.WriteAttributeString("transport", "http://schemas.xmlsoap.org/soap/http");
                xw.WriteEndElement();

                WriteWsdlSoap12OperationBinding("StartProcess", xw, ctx);

                foreach (Task t in pd.Body.GetTasksRecursive())
                {
                    string tid = t.Id.Replace('.', '_');
                    WriteWsdlSoap12OperationBinding(string.Format("UpdateTaskData_{0}", tid), xw, ctx);
                    WriteWsdlSoap12OperationBinding(string.Format("CompleteTask_{0}", tid), xw, ctx);
                    WriteWsdlSoap12OperationBinding(string.Format("GetTaskData_{0}", tid), xw, ctx);
                }


                xw.WriteEndElement();

            }//end soap 12 binding
            {//service
                xw.WriteStartElement("service", XmlSchemaUtil.WSDL_NS);
                xw.WriteAttributeString("name", "Proc2");
                
                xw.WriteStartElement("port", XmlSchemaUtil.WSDL_NS);
                xw.WriteAttributeString("name", "Proc2Soap");
                xw.WriteAttributeString("binding", "tns:Proc2Soap");
                xw.WriteStartElement("address", XmlSchemaUtil.SOAP_NS);
                xw.WriteAttributeString("location", ctx.Url.ToString().Replace(ctx.Url.Query, ""));
                xw.WriteEndElement();
                xw.WriteEndElement();

                xw.WriteStartElement("port", XmlSchemaUtil.WSDL_NS);
                xw.WriteAttributeString("name", "Proc2Soap12");
                xw.WriteAttributeString("binding", "tns:Proc2Soap12");
                xw.WriteStartElement("address", XmlSchemaUtil.SOAP12_NS);
                xw.WriteAttributeString("location", ctx.Url.ToString().Replace(ctx.Url.Query, ""));
                xw.WriteEndElement();
                xw.WriteEndElement();

                xw.WriteEndElement();
            }

            xw.WriteEndElement();
            xw.WriteEndDocument();
            xw.Flush();
        }

        private string GenerateWebServiceSchemaXml(ProcessDefinition pd)
        {
            TypeSet allTypes = new TypeSet();
            allTypes.ImportTypes(pd.DataTypes);
            pd.Body.GetAllTaskDataSchemas(allTypes);


            StringBuilder sb = new StringBuilder();
            XmlWriterSettings ws = new XmlWriterSettings();
            ws.OmitXmlDeclaration = true; ws.Indent = true;
            XmlWriter xw = XmlWriter.Create(sb, ws);
            xw.WriteStartDocument();
            xw.WriteStartElement("processService");

            xw.WriteAttributeString("wsNamespace", string.Format("http://nginn.org/process/{0}", pd.DefinitionId));
            xw.WriteAttributeString("definitionId", pd.DefinitionId);
            xw.WriteAttributeString("processName", pd.LocalName);
            xw.WriteAttributeString("mainTaskId", pd.Body.Id);
            {
                xw.WriteStartElement("Tasks");
                foreach (NetNode nn in pd.Body.GetNodesRecursive())
                {
                    if (!(nn is Task)) continue;
                    xw.WriteStartElement("Task");
                    xw.WriteAttributeString("id", nn.Id);
                    xw.WriteEndElement();
                }
                xw.WriteEndElement();
            }

            xw.WriteStartElement("dataTypes");
            xw.WriteStartElement("xs", "schema", XmlSchemaUtil.SCHEMA_NS);
            allTypes.WriteXmlSchema(xw);
            xw.WriteEndElement();
            xw.WriteEndElement();
            xw.WriteEndDocument();
            xw.Flush();
            return sb.ToString();
        }

        private void OutputServiceWSDL(TextReader input, TextWriter output, IRequestContext ctx)
        {
            string dn = ctx.UrlVariables["definition"];
            ProcessDefinition proc = PackageRepository.GetProcess(dn);
            if (proc == null)
                throw new Exception("Process definition not found: " + dn);
            //string s = GenerateWebServiceSchemaXml(proc);
            //output.Write(s);
            GenerateWsdl(proc, output, ctx);
        }

        private void HandleWebServiceInvocation(TextReader input, TextWriter output, IRequestContext ctx)
        {
            XmlDocument doc = new XmlDocument();
            doc.Load(input);
            log.Warn("Incoming message: {0}", doc.OuterXml);
            
            XmlNamespaceManager nsmgr = new XmlNamespaceManager(doc.NameTable);
            XmlElement el = RetrieveSoapMessagePayload(doc.DocumentElement, nsmgr);
            log.Info("Message body: {0}", el.OuterXml);
            if (el.Name == "StartProcess")
            {
                HandleStartProcess(el);
            }
            else if (el.Name == "")
            {
            }
            else throw new Exception();
        }

        private void HandleStartProcess(XmlElement el)
        {
            if (el.LocalName != "StartProcess") throw new Exception();
            string defid = null;
            XmlElement input = null;
            string startedBy = null;
            string corrId = null;
            foreach (XmlNode xn in el.ChildNodes)
            {
                if (xn is XmlElement)
                {
                    if (xn.LocalName == "createdBy")
                        startedBy = xn.InnerText;
                    else if (xn.LocalName == "correlationId")
                        corrId = xn.InnerText;
                    else if (xn.LocalName == "input")
                        input = (XmlElement)xn;
                    else
                        throw new Exception("Unexpected node: " + xn.Name);
                  
                }
                else throw new Exception("Unexpected child node: " + xn.Name);
            }
            string ret = Environment.StartProcess(defid, DataUtil.ReadXml(new StringReader( input.OuterXml)), startedBy, corrId);
        }

        private XmlElement RetrieveSoapMessagePayload(XmlElement el, XmlNamespaceManager nsmgr)
        {
  
            if (el.NamespaceURI != XmlSchemaUtil.SOAP_ENV)
                throw new Exception("Unexpected namespace: " + el.NamespaceURI);
            if (el.LocalName != "Envelope")
                throw new Exception("Expected soap envelope");

            string prf = el.OwnerDocument.GetPrefixOfNamespace(el.NamespaceURI);
            log.Info("Prefix: {0}", prf);
            
            foreach (XmlNode xn in el.ChildNodes)
            {
                if (xn is XmlElement)
                {
                    if (xn.LocalName != "Body")
                        throw new Exception("SOAP Body expected");
                    else
                    {
                        foreach (XmlNode xn2 in ((XmlElement) xn).ChildNodes)
                        {
                            if (xn2 is XmlElement)
                                return xn2 as XmlElement;
                        }
                    }
                }
            }
            throw new Exception("Failed to find message content");
        }

        private void OutputServiceInformation(TextReader input, TextWriter output, IRequestContext ctx)
        {
        }
    }
}
